package knyet.appframe;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.graphics.PixelFormat;
import android.os.Environment;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;

public class GLSurfaceView extends SurfaceView implements Callback {

	private static final boolean DEBUG = true;
	private static final String TAG = "GLSurfaceView";
	private static final String TAG_RENDER_RUNNABLE = "GLSurfaceView.RenderRunnable";

	private IRenderer renderer;
	private RenderRunnable renderRunnable;
	private Semaphore renderThreadStartSemaphore;
	private Semaphore renderThreadDestroySemaphore;
	private Semaphore surfaceReadySemaphore;
	private Semaphore glInitializeSemaphore;
	private int surfaceWidth = 0;
	private int surfaceHeight = 0;
	private AssetManager assetMgr;
	
	
	private class TouchEvent implements Runnable {
		private int mMask;
		private float mX;
		private float mY;
		TouchEvent(int mask, float x, float y) {mMask = mask; mX = x; mY = y;}
		public void run() {
			if (null != renderer) {
				if ((mMask & MotionEvent.ACTION_MOVE) != 0)
					renderer.OnTouchMove(mX, mY);
				else if ((mMask & MotionEvent.ACTION_UP) != 0) 
					renderer.OnTouchUp(mX, mY);
				else if (mMask == MotionEvent.ACTION_DOWN) 
					renderer.OnTouchDown(mX, mY);
			}
		}
	}
	private class JoyStickAnologEvent implements Runnable {
		private int mAxis;
		private float mValueX;
		private float mValueY;
		JoyStickAnologEvent(float x, float y, int axis) {mValueX = x; mValueY = y; mAxis = axis;}
		public void run() {
			renderer.OnPS3DS_AnalogInput(mValueX, mValueY, mAxis);
		}	
	}
	private class JoyStickButtonEvent implements Runnable {
		private int mBottonId;
		private boolean mDownOrUp;
		JoyStickButtonEvent(boolean down_up, int keyCode) {mDownOrUp = down_up; mBottonId = keyCode;}
		public void run() {
			renderer.OnPS3DS_BottonInput(mDownOrUp, mBottonId);
		}	
	}
	private class ChangeDbgStringEvent implements Runnable {
		private String mDbgString;
		ChangeDbgStringEvent(String str) {mDbgString = str;}
		public void run() {
			renderer.SetDebugString(mDbgString);
		}	
	}
	private void _ShowDbgString(String str) {
		renderRunnable.queueEvent(new ChangeDbgStringEvent(str));
	}
	
	/**************************************************************************
	 ************** These for the various Event handlers***********************
	 **************************************************************************/
	private OnTouchListener _touchListener = new OnTouchListener() {
		public boolean onTouch(View v, MotionEvent event) {
			final int eventMask = event.getActionMasked();
			final int pointerCount = event.getPointerCount();
			
			if (pointerCount > 0) {
				if ((eventMask & MotionEvent.ACTION_MOVE) != 0) {
					// Need to consider the historical movements
					final int historySize = event.getHistorySize();
					for (int h = historySize - 1; h >= 0 ; h--) {
						renderRunnable.queueEvent(new TouchEvent(MotionEvent.ACTION_MOVE, event.getHistoricalX(h), event.getHistoricalY(h)));
					}
					renderRunnable.queueEvent(new TouchEvent(MotionEvent.ACTION_MOVE, event.getX(), event.getY()));
				}
				else if ((eventMask & MotionEvent.ACTION_UP) != 0) 
					renderRunnable.queueEvent(new TouchEvent(MotionEvent.ACTION_UP, event.getX(), event.getY()));
				else if (eventMask == MotionEvent.ACTION_DOWN) 
					renderRunnable.queueEvent(new TouchEvent(MotionEvent.ACTION_DOWN, event.getX(), event.getY()));
			}
			
			//_ShowDbgString(String.format("action %x", event.getAction()));
			
			return true;
		}
	};
	private OnKeyListener _keyListener = new OnKeyListener() {
		public boolean onKey(View v, int keyCode, KeyEvent event) {
			//_ShowDbgString(String.format("keycode %x", keyCode));
			
			//if (keyCode < KeyEvent.KEYCODE_BUTTON_1 || keyCode > KeyEvent.KEYCODE_BUTTON_16)
				//return false;	// Ignore other key inputs
			
			if (event.getAction() == KeyEvent.ACTION_DOWN) {
				renderRunnable.queueEvent(new JoyStickButtonEvent(true, keyCode));
				return true;
			}
			else if (event.getAction() == KeyEvent.ACTION_UP) {
				renderRunnable.queueEvent(new JoyStickButtonEvent(false, keyCode));
				return true;
			}
		
			
			return false;
		}
	};
	public boolean onGenericMotionEvent(MotionEvent event) {

		renderRunnable.queueEvent(
				new JoyStickAnologEvent(event.getAxisValue(MotionEvent.AXIS_X, 0), 
						event.getAxisValue(MotionEvent.AXIS_X, 0), 
						0));
		renderRunnable.queueEvent(
				new JoyStickAnologEvent(event.getAxisValue(MotionEvent.AXIS_Z, 0), 
						event.getAxisValue(MotionEvent.AXIS_RZ, 0), 
						1));
		renderRunnable.queueEvent(
				new JoyStickAnologEvent(event.getAxisValue(MotionEvent.AXIS_LTRIGGER, 0), 
						event.getAxisValue(MotionEvent.AXIS_RTRIGGER, 0), 
						2));
		/*Iterator<InputDevice.MotionRange> iter = event.getDevice().getMotionRanges().iterator();
		String outputStr = "";
		while (iter.hasNext()) {
			InputDevice.MotionRange mr = iter.next();
			outputStr += String.format("(%x, %.2f) ", mr.getAxis(), event.getAxisValue(mr.getAxis(), 0));
		}
		_ShowDbgString(outputStr); */
		return false;
	}
	/**************************************************************************
	 ************** End of Event Handlers **************************************
	 **************************************************************************/
	
	public GLSurfaceView(Context context) {
		super(context);

		
		assetMgr = context.getAssets();
		SurfaceHolder holder = getHolder();
		holder.addCallback(this);

		// This is important!
		// Not doing this will cause failure when eglCreateWindowSurface()
		holder.setFormat(PixelFormat.RGBA_8888);
	}

	public void setRenderer(IRenderer renderer) {
		this.renderer = renderer;
	}
	
	public void surfaceCreated(SurfaceHolder holder) {

		// Configure the event listeners
		setOnTouchListener(_touchListener);
		setOnKeyListener(_keyListener);
		setFocusable(true);
		
		
		PackageInfo info = null;
		try {
		   info = getContext().getPackageManager().getPackageInfo("knyet.appframe", 0);
		} catch( NameNotFoundException e ) {
		   Log.e( TAG, e.toString() );
		   return;
		}
		File sdRoot = Environment.getExternalStorageDirectory();
		String appFolder = "/" + sdRoot.getName() + "/" + info.applicationInfo.packageName + "/";
		renderer.SetAppDirectory(appFolder, assetMgr);
		
		if (DEBUG) {
			Log.d(TAG, "pre surfaceCreated()...");
		}

		renderRunnable = new RenderRunnable();
		renderThreadStartSemaphore = new Semaphore(0);
		renderThreadDestroySemaphore = new Semaphore(0);
		surfaceReadySemaphore = new Semaphore(0);
		glInitializeSemaphore = new Semaphore(0);

		_ShowDbgString("This is Android platform");
		// Start thread and wait until it starts
		new Thread(renderRunnable, "GL_Thread").start();
		renderThreadStartSemaphore.acquireUninterruptibly();

		if (DEBUG) {
			Log.d(TAG, "post surfaceCreated()");
		}
	}

	public void surfaceDestroyed(SurfaceHolder holder) {
		if (DEBUG) {
			Log.d(TAG, "pre surfaceDestroyed()...");
		}

		renderRunnable.destroy();
		renderThreadDestroySemaphore.acquireUninterruptibly();

		if (DEBUG) {
			Log.d(TAG, "post surfaceDestroyed()");
		}
	}

	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {

		if (width > 0 && height > 0
				&& (width != this.surfaceWidth || height != this.surfaceHeight)) {

			if (0 == this.surfaceWidth && 0 == this.surfaceHeight) {

				this.surfaceWidth = width;
				this.surfaceHeight = height;

				// Wait until GL initialized
				glInitializeSemaphore.acquireUninterruptibly();

				if (DEBUG) {
					Log.d(TAG, "Surface ready: w = " + width + ", h = "
							+ height);
				}

				// Surface ready, notify the render thread
				// that it may enter main loop now
				surfaceReadySemaphore.release();

			} else {

				this.surfaceWidth = width;
				this.surfaceHeight = height;

				if (DEBUG) {
					Log.d(TAG, "Surface resize: w = " + width + ", h = "
							+ height);
				}

				renderRunnable.queueEvent(new ResizeEvent());
			}
		}

	}

	/**
	 * Called when Activity paused
	 */
	public void onPause() {
		if (null != renderRunnable) {
			renderRunnable.pause();
		}
	}

	/**
	 * Called when Activity resumed
	 */
	public void onResume() {
		if (null != renderRunnable) {
			renderRunnable.resume();
		}
	}
	private class RenderRunnable implements Runnable {

		private Thread renderThread;
		private volatile boolean rendering = false;
		private AtomicBoolean paused = new AtomicBoolean(false);
		private ArrayList<Runnable> eventQueue = new ArrayList<Runnable>();

		private EGL10 egl;
		private EGLDisplay eglDisplay;
		private EGLSurface eglSurface;
		private EGLContext eglContext;

		public void run() {

			renderThread = Thread.currentThread();
			renderThreadStartSemaphore.release();

			if (initEGL()) {

				if (DEBUG) {
					Log.d(TAG_RENDER_RUNNABLE, "EGL initialized");
				}

				if (null != renderer) {
					if (!renderer.Initialize()) {
						Log.e(TAG_RENDER_RUNNABLE,
								"Initialize() failed....");
					}
				}
				glInitializeSemaphore.release();

			} else {

				Log.e(TAG_RENDER_RUNNABLE,
						"initEGL() failed. GL Thread exiting...");

				glInitializeSemaphore.release();
				renderThreadDestroySemaphore.release();
				return;
			}

			// wait until the size of surface is ready
			try {
				surfaceReadySemaphore.acquire();

				if (DEBUG) {
					Log.d(TAG_RENDER_RUNNABLE,
							"Surface ready. Resizing GL scene then entering main loop...");
				}

				if (null != renderer) {
					renderer.Resize(surfaceWidth, surfaceHeight);
				}
			} catch (InterruptedException e) {

				if (DEBUG) {
					Log.d(TAG_RENDER_RUNNABLE,
							"GL Thread interrupted on waiting for surface ready");
				}

				destroyEGL();
				renderThreadDestroySemaphore.release();
				return;
			}
			
			
			{ 
				
				// The main loop
				for (rendering = true; rendering;) {
	
					// Process events
					synchronized (eventQueue) {
						while (eventQueue.size() > 0) {
							Runnable e = eventQueue.remove(0);
							e.run();
						}
					}
	
					// Check pause flag
					synchronized (paused) {
						if (paused.get()) {
							try {
								paused.wait();
							} catch (InterruptedException e) {
								if (DEBUG) {
									Log.d(TAG_RENDER_RUNNABLE,
											"GL Thread interrupted on paused state");
								}
								break;
							}
						}
					}
	
					// Render a single frame
					// Log.d(TAG_RENDER_RUNNABLE, "Rendering a single frame...");
					if (null != renderer) {
						if (!renderer.TimeElapse())
							break;
						egl.eglSwapBuffers(eglDisplay, eglSurface);
					}
	
				}// main loop
				
				renderer.Destory();
			}

			if (DEBUG) {
				Log.d(TAG_RENDER_RUNNABLE, "GL Thread exiting...");
			}

			destroyEGL();
			renderThreadDestroySemaphore.release();
		}


		public void queueEvent(Runnable e) {
			if (rendering) {
				synchronized (eventQueue) {
					eventQueue.add(e);
				}
			}
		}

		public void resume() {
			if (rendering) {
				synchronized (paused) {
					if (paused.get()) {
						paused.set(false);
						paused.notifyAll();
					}
				}
			}
		}

		public void pause() {
			if (rendering) {
				synchronized (paused) {
					if (!paused.get()) {
						paused.set(true);
					}
				}
			}
		}

		public void destroy() {
			rendering = false;
			if (null != renderThread) {
				renderThread.interrupt();
			}

		}

		private boolean initEGL() {

			egl = (EGL10) EGLContext.getEGL();

			//
			eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
			if (EGL10.EGL_NO_DISPLAY == eglDisplay) {
				Log.e(TAG_RENDER_RUNNABLE, "eglGetDisplay() failed");
				destroyEGL();
				return false;
			}

			//
			int[] eglVersions = new int[2];
			if (egl.eglInitialize(eglDisplay, eglVersions)) {
				if (DEBUG) {
					Log.d(TAG_RENDER_RUNNABLE, "EGL version = "
							+ eglVersions[0] + "." + eglVersions[1]);
				}
			} else {
				Log.e(TAG_RENDER_RUNNABLE, "eglInitialize() failed");
				destroyEGL();
				return false;
			}

			//
			EGLConfig eglConfig;
			int[] attrList = new int[] { //
			EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, //
					EGL10.EGL_RED_SIZE, 8, //
					EGL10.EGL_GREEN_SIZE, 8, //
					EGL10.EGL_BLUE_SIZE, 8, //
					//EGL10.EGL_DEPTH_SIZE, 16, //
					EGL10.EGL_NONE //
			};
			EGLConfig[] configs = new EGLConfig[1];
			int[] numConfig = new int[1];
			if (egl.eglChooseConfig(eglDisplay, attrList, configs, 1, numConfig)
					&& numConfig[0] > 0) {
				eglConfig = configs[0];
			} else {
				Log.e(TAG_RENDER_RUNNABLE, "eglChooseConfig() failed");
				destroyEGL();
				return false;
			}

			//
			eglContext = egl.eglCreateContext(eglDisplay, eglConfig,
					EGL10.EGL_NO_CONTEXT, null);
			if (EGL10.EGL_NO_CONTEXT == eglContext) {
				Log.e(TAG_RENDER_RUNNABLE, "eglCreateContext() failed");
				destroyEGL();
				return false;
			}

			//
			eglSurface = egl.eglCreateWindowSurface(eglDisplay, eglConfig,
					getHolder(), null);
			if (EGL10.EGL_NO_SURFACE == eglSurface) {
				Log.e(TAG_RENDER_RUNNABLE, "eglCreateWindowSurface() failed");
				destroyEGL();
				return false;
			}

			//
			if (!egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface,
					eglContext)) {
				Log.e(TAG_RENDER_RUNNABLE, "eglMakeCurrent() failed");
				destroyEGL();
				return false;
			}

			return true;
		}

		private void destroyEGL() {

			if (null == egl) {
				return;
			}

			if (null != eglDisplay && EGL10.EGL_NO_DISPLAY != eglDisplay) {

				egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE,
						EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);

				if (null != eglSurface && EGL10.EGL_NO_SURFACE != eglSurface) {
					egl.eglDestroySurface(eglDisplay, eglSurface);
					eglSurface = null;
				}

				if (null != eglContext && EGL10.EGL_NO_CONTEXT != eglContext) {
					egl.eglDestroyContext(eglDisplay, eglContext);
					eglContext = null;
				}

				egl.eglTerminate(eglDisplay);
				eglDisplay = null;
			}

			egl = null;
		}

	}

	private class ResizeEvent implements Runnable {

		public void run() {
			if (null != renderer) {
				renderer.Resize(surfaceWidth, surfaceHeight);
			}
		}

	}
}
